package bancosys.tec.persist.hibernate.page;

import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.hibernate.Criteria;
import org.hibernate.Query;
import org.hibernate.ScrollableResults;
import org.hibernate.Session;
import org.hibernate.criterion.Projection;
import org.hibernate.criterion.Projections;
import org.hibernate.impl.CriteriaImpl;
import org.hibernate.impl.CriteriaImpl.OrderEntry;
import org.hibernate.transform.ResultTransformer;

import bancosys.tec.persist.dao.BaseDAO;

/**
 * @created Oct 16, 2006
 * @author Gustavo Almeida
 */
public class PagingService {

    private static final int SELECT_SIZE = 6;

    private ThreadLocal<PagingData> pagingData = new ThreadLocal<PagingData>();

    private ThreadLocal<Boolean> pagingEnabled = new ThreadLocal<Boolean>();

    private OrderCriteriaService orderCriteriaService = new OrderCriteriaService();

    /**
     * Atualiza a criteria para utilizar paginação na recuperação dos dados.
     * 
     * @param criteria a Criteria utilizada para a recuperação dos dados.
     */
    public void paginate(Criteria criteria) {
        PagingData data = this.getPagingData();
        if (data == null) {
            return;
        }

        if (data.getItensPerPage() > 0) {
            if (!data.isDataInitialized() || data.isIncomplete()) {
                setPagingDataItemCount(criteria, data);
            }
            criteria.setFirstResult(data.getItensPerPage() * data.getCurrentPage());
            criteria.setMaxResults(data.getItensPerPage());
        }
        if (data.getOrderBy() != null) {
            this.orderCriteriaService.order(criteria, data);
        }
    }

    /**
     * Pagina a query e executa
     * 
     * @param criteria {@link Criteria}
     * @return {@link List}
     */
    public List<?> paginateExecuteQuery(Criteria criteria) {
        PagingData data = this.getPagingData();
        if (data == null) {
            return criteria.list();
        }
        if (data.getItensPerPage() > 0) {
            if (!data.isDataInitialized()) {
                setPagingDataItemCount(criteria, data);
            }
            criteria.setFirstResult(data.getItensPerPage() * data.getCurrentPage());
            criteria.setMaxResults(data.getItensPerPage());
        }
        if (data.getOrderBy() != null) {
            this.orderCriteriaService.order(criteria, data);
        }
        return criteria.list();
    }

    /**
     * Seta no {@link PagingData} o numero maximo de elementos devolvidos pelo {@link Criteria} passado
     * 
     * @param criteria {@link Criteria}
     * @param data {@link PagingData}
     */
    private void setPagingDataItemCount(Criteria criteria, PagingData data) {
        if (criteria instanceof CriteriaImpl) {
            CriteriaImpl criteriaImpl = (CriteriaImpl) criteria;
            List<OrderEntry> orders = new ArrayList<OrderEntry>();
            Iterator orderings = criteriaImpl.iterateOrderings();
            while (orderings.hasNext()) {
                orders.add((OrderEntry) orderings.next());
                orderings.remove();
            }
            Projection oldProjection = criteriaImpl.getProjection();
            ResultTransformer transformer = criteriaImpl.getResultTransformer();
            try {
                criteria.setProjection(Projections.rowCount());
                data.setTotalItens(((Number) criteria.list().get(0)).longValue());
            } finally {
                criteria.setProjection(oldProjection);
                criteria.setResultTransformer(transformer);
                for (OrderEntry orderEntry : orders) {
                    orderEntry.getCriteria().addOrder(orderEntry.getOrder());
                }
            }
        }
    }

    /**
     * Devolve uma nova query, baseada na query passada como parâmetro, que utiliza paginação na recuperação dos dados.
     * 
     * @param session a sessão.
     * @param query a query utilizada para a recuperação dos dados.
     * @return uma nova query com paginação.
     */
    public Query paginate(Session session, String query) {
        return this.paginate(session, query, null);
    }

    /**
     * Devolve uma nova query, baseada na query passada como parâmetro, que utiliza paginação na recuperação dos dados.
     * 
     * @param session a sessão.
     * @param query a query utilizada para a recuperação dos dados.
     * @param properties os parâmetros que serão adicionados a query.
     * @return uma nova query com paginação.
     */
    @SuppressWarnings("deprecation")
    public Query paginate(Session session, String query, Map<String, Object> properties) {
        PagingData data = this.getPagingData();
        if (data == null) {
            Query queryObject = session.createQuery(query);
            BaseDAO.bindParameters(queryObject, properties);
            return queryObject;
        }

        String newQuery = query;
        if (data.getOrderBy() != null) {
            int indexOrderBy = newQuery.toUpperCase().lastIndexOf("ORDER BY");
            if (indexOrderBy > -1) {
                newQuery = newQuery.substring(0, indexOrderBy);
            }

            int indexSelect = newQuery.toUpperCase().lastIndexOf("SELECT");

            String attributeSelect = null;
            if (indexSelect > -1) {
                // XXX (finx) Nesse caso está aceitando apenas um atributo entre o "select" e o "from".
                int indexFrom = newQuery.toUpperCase().lastIndexOf("FROM");
                attributeSelect = newQuery.substring(indexSelect + SELECT_SIZE, indexFrom).trim();
            }

            String attributeOrderBy = attributeSelect == null ? data.getOrderBy() : attributeSelect + "." + data.getOrderBy();
            if (data.isOrderAscending()) {
                newQuery += " ORDER BY " + attributeOrderBy + " ASC";
            } else {
                newQuery += " ORDER BY " + attributeOrderBy + " DESC";
            }
        }

        Query queryObject = session.createQuery(newQuery);
        BaseDAO.bindParameters(queryObject, properties);

        if (data.getItensPerPage() > 0) {
            if (!data.isDataInitialized()) {
                ScrollableResults scrollableResults = queryObject.scroll();
                scrollableResults.last();
                data.setTotalItens(scrollableResults.getRowNumber() + 1);
                scrollableResults.close();
            }
            queryObject.setMaxResults(data.getItensPerPage());
            queryObject.setFirstResult(data.getItensPerPage() * data.getCurrentPage());
        }
        return queryObject;
    }

    /**
     * Limpa os dados para paginação.
     */
    public void clearPagingData() {
        this.pagingData.set(null);
    }

    /**
     * Devolve os dados para paginação.
     * 
     * @return os dados para paginação.
     */
    public PagingData getPagingData() {
        if (this.isPagingEnabled() != null && !this.isPagingEnabled()) {
            return null;
        }
        return this.pagingData.get();
    }

    /**
     * Define os dados para paginação.
     * 
     * @param pagingData os dados para paginação.
     */
    public void setPagingData(PagingData pagingData) {
        this.pagingData.set(pagingData);
    }

    /**
     * Informa se a paginacao esta habilitada na thread corrente
     * 
     * @return <code>true</code> se a paginacao estiver habilitada na thread corrente, <code>false</code> caso contrario.
     */
    public Boolean isPagingEnabled() {
        return this.pagingEnabled.get();
    }

    /**
     * Define se a paginacao deve ficar habilitada/desabilitada na thread corrente.
     * 
     * @param pagingEnabled the pagingEnabled to set
     */
    public void setPagingEnabled(Boolean pagingEnabled) {
        this.pagingEnabled.set(pagingEnabled);
    }
}